package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import network.networkManager.NetworkManager;
import network.networkProfile.NetworkProfile;
import util.ConsoleMonitor;

/**
 * manages IO connections between multiple hosts, abstracts out the actual network
 * <br><br>
 * a network manager consists of 2 threads, both threads are daemons
 * <br><br>
 * thread 1: receives udp data from the associated port<br>
 * thread 2: updates the network profiles
 * @author jack
 *
 */
public final class Network extends UDPDaemon
{
	DatagramSocket s;
	Map<Integer, NetworkProfile> profiles = Collections.synchronizedMap(new HashMap<Integer, NetworkProfile>()); //the keys are the hashes of the inet addresses
	List<NetworkProfile> profileList = Collections.synchronizedList(new ArrayList<NetworkProfile>()); //list of profiles for faster traversal
	List<NetworkListener> listeners = Collections.synchronizedList(new ArrayList<NetworkListener>(5)); //will probably only have 1 listener
	
	/**
	 * starts a new network manager
	 * @param portO
	 * @throws IOException
	 */
	public Network(int port) throws IOException
	{
		super(port);
		s = new DatagramSocket();
		
		Thread t = new Thread(new Runnable(){
			public void run()
			{
				try
				{
					int i = 0;
					for(;;)
					{
						update();
						i++;
						if(i % 4000 == 0)
						{
							System.out.println("=====================");
							System.out.println(getProfileData());
							System.out.println("=====================");
						}
						try
						{
							Thread.sleep(10);
						}
						catch(InterruptedException e){}
					}
				}
				finally
				{
					s.close();
				}
			}
		});
		t.setDaemon(true);
		t.start();
	}
	/**
	 * gets the profile map, the map itself is synchronized, map
	 * keys are the hashes of the InetAddress associated with each profile
	 * @return returns the profile map
	 */
	public Map<Integer, NetworkProfile> getProfiles()
	{
		return profiles;
	}
	/**
	 * interacts with the synchronized list of listeners, the list should be
	 * used often so it is best to add listeners prior to starting the connections
	 * to avoid a slight hang to add the listener
	 * @param nl
	 */
	public void addListener(NetworkListener nl)
	{
		listeners.add(nl);
	}
	/**
	 * sends a batch of data to the specified address
	 * @param batch the data batch to be sent
	 * @param address the address to send the data to
	 */
	public void send(SendBatch batch, InetAddress address)
	{
		try
		{
			profiles.get(address.hashCode()).send(batch);
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
	}
	public void addProfile(InetAddress address, int port)
	{
		try
		{
			NetworkProfile np = new NetworkProfile(s, address, port);
			profiles.put(address.hashCode(), np);
			profileList.add(np);
			System.out.println("profile added for address: "+address+", port = "+port);
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
	}
	protected void packetReceived(DatagramPacket p)
	{
		//System.out.println("received packet, address = "+p.getAddress()+", hash = "+p.getAddress().hashCode());
		//System.out.println(profiles.get(p.getAddress().hashCode()));
		//System.out.println("------------------------------------");
		
		byte[] data = p.getData();
		ByteBuffer b = ByteBuffer.wrap(data);
		if(profiles.get(p.getAddress().hashCode()).processHeader(b))
		{
			for(NetworkListener nl: listeners)
			{
				nl.packetReceived(new NetworkEvent(p.getAddress(), b));
			}
		}
	}
	public void update()
	{
		for(Integer key: profiles.keySet())
		{
			profiles.get(key).update();
		}
	}
	public static void main(String[] args)
	{
		/*
		 * runs a client server data test, however its hard to tell what happens because the client
		 * and server both print to the same console, should running client and server separately to debug
		 */
		new ConsoleMonitor();
		try
		{
			Network server = new Network(NetworkManager.serverPort);
			Network client = new Network(NetworkManager.clientPort);
			InetAddress clientAddress = InetAddress.getLocalHost();
			server.addProfile(clientAddress, NetworkManager.clientPort);
			client.addProfile(InetAddress.getLocalHost(), NetworkManager.serverPort);
			System.out.println("===============================================");
			
			Charset c = Charset.forName("UTF-8");
			for(int i = 0; i < 10; i++)
			{
				SendBatch batch = new SendBatch();
				batch.add(c.encode("simple test message!!!").array(), true);
				System.out.println("batch = "+batch+"\n------------");
				server.send(batch, clientAddress);
				for(int a = 0; a < 10; a++)
				{
					server.update();
					
					try
					{
						Thread.sleep(10);
					}
					catch(InterruptedException q){}
				}
				System.out.println("===========");
				System.out.println("server:");
				System.out.println(server.profiles.get(clientAddress.hashCode()));
				System.out.println("===========");
				try
				{
					Thread.sleep(200);
				}
				catch(InterruptedException d){}
			}
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
	}
	private String getProfileData()
	{
		String s = "";
		int i = 1;
		for(NetworkProfile p: profileList)
		{
			s+="profile "+i+":\n";
			s+=p;
			i++;
		}
		return s;
	}
}
